package com.yhy.gateway.config;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.net.NetUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.nimbusds.jose.JWSObject;
import com.yhy.common.core.exception.BizException;
import com.yhy.common.core.result.Result;
import com.yhy.common.core.result.ResultEnum;
import com.yhy.common.redis.utils.RedisUtil;
import com.yhy.common.security.config.PermitAllProperties;
import com.yhy.common.security.constant.SecurityConstants;
import com.yhy.common.security.model.Rule;
import com.yhy.gateway.utils.ResponseUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.util.Strings;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.server.authorization.AuthorizationContext;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

import java.text.ParseException;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;


/**
 * 授权管理器
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class AuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {

    final PermitAllProperties permitAllProperties;

    @Override
    public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {
        ServerHttpRequest request = authorizationContext.getExchange().getRequest();
        String accessIp = NetUtil.getLocalhostStr();

        log.debug("{} -- [{}] {} {}",
                accessIp, request.getMethodValue(), request.getPath(), JSONUtil.parse(request.getQueryParams()));

        // ip黑名单
        Set<Object> cacheBlackListIps = Optional.ofNullable(
                RedisUtil.sGet(SecurityConstants.CACHE_BLACK_LIST_IPS)).orElse(new HashSet<>());
        if (CollUtil.isNotEmpty(cacheBlackListIps) && cacheBlackListIps.contains(accessIp)) {
            log.error("当前ip: {} 访问被拒绝", accessIp);
            return Mono.just(new AuthorizationDecision(false));
        }

        // 预检请求放行
        if (request.getMethod() == HttpMethod.OPTIONS) {
            return Mono.just(new AuthorizationDecision(true));
        }
        // 如果token以"bearer "为前缀，到此方法里说明JWT有效即已认证
        String token = request.getHeaders().getFirst(SecurityConstants.AUTHORIZATION_KEY);
        if (StrUtil.isBlank(token) || !StrUtil.startWithIgnoreCase(token, SecurityConstants.JWT_PREFIX)) {
            log.error("token不合法, 访问被拒绝! token: {}", token);
            return Mono.just(new AuthorizationDecision(false));
        }
        String payload;
        try {
            token = StrUtil.replaceIgnoreCase(token, SecurityConstants.JWT_PREFIX, Strings.EMPTY);
            payload = StrUtil.toString(JWSObject.parse(token).getPayload());
        } catch (ParseException e) {
            log.error("token解析失败: {}", token);
            return Mono.just(new AuthorizationDecision(false));
        }

        JSONObject jsonObject = JSONUtil.parseObj(payload);
        // 解析JWT获取jti，以jti为key判断redis的黑名单列表是否存在，存在则拦截访问
        String jti = jsonObject.getStr(SecurityConstants.JWT_JTI);
        if (!RedisUtil.hasKey(SecurityConstants.CACHE_REDIS_ONLINE_USER + jti)) {
            log.error("黑名单JTI: {}", jti);
            return Mono.just(new AuthorizationDecision(false));
        }
        String clientId = jsonObject.getStr(SecurityConstants.AUTHORIZATION_CLIENT);
        // 白名单客户端
        if (permitAllProperties.getClients().contains(clientId)) {
            return Mono.just(new AuthorizationDecision(true));
        }

        /*
         * 鉴权开始
         *
         * 缓存取 [URL权限-角色集合] 规则数据
         * urlPermRolesRules = [{'key':'GET:/api/v1/users/*','value':['ADMIN','TEST']},...]
         */
        String restfulPath = request.getMethodValue() + ":" + request.getURI().getPath();
        if (!RedisUtil.hHasKey(SecurityConstants.CACHE_URL_PERM_ROLES_KEY, restfulPath)) {
            // 没有限制权限
            return Mono.just(new AuthorizationDecision(true));
        }

        // 拥有访问权限的角色
        final String ruleStr = JSONUtil.toJsonStr(RedisUtil.hGet(SecurityConstants.CACHE_URL_PERM_ROLES_KEY, restfulPath));

        Rule rule = JSONUtil.toBean(ruleStr, Rule.class);
        // 白名单
        List<String> whiteListIps = rule.getWhiteListIps();
        // 黑名单
        List<String> blackListIps = rule.getBlackListIps();
        // 角色
        List<String> roles = rule.getRoles();

        if (CollUtil.isNotEmpty(whiteListIps) && !whiteListIps.contains(accessIp)) {
            // 当前ip没有访问权限
            log.error("当前ip: {} {} 没有访问权限", accessIp, restfulPath);
            return Mono.just(new AuthorizationDecision(false));
        }

        if (CollUtil.isNotEmpty(blackListIps) && blackListIps.contains(accessIp)) {
            // 当前ip没有访问权限
            log.error("当前ip: {} {} 访问被拒绝", accessIp, restfulPath);
            return Mono.just(new AuthorizationDecision(false));
        }

        roles.add(SecurityConstants.ROOT_ROLE_CODE);
        // 判断JWT中携带的用户角色是否有权限访问
        return mono.filter(Authentication::isAuthenticated)
                .flatMapIterable(Authentication::getAuthorities)
                .map(GrantedAuthority::getAuthority)
                .map(authority -> authority.substring(SecurityConstants.AUTHORITY_PREFIX.length()))
                .any(roles::contains)    // 拥有访问权限的角色中包含该用户的角色
                .map(AuthorizationDecision::new)
                .defaultIfEmpty(new AuthorizationDecision(false));
    }

}
